Skip to content

feat(group): member-add-mode, join-approval-mode, participant requests + message forward - #2595

Open
alpesdigital-adm wants to merge 22 commits into
evolution-foundation:mainfrom
alpesdigital-adm:feat/group-admin-controls-and-forward
Open

alpesdigital-adm wants to merge 22 commits into
evolution-foundation:mainfrom
alpesdigital-adm:feat/group-admin-controls-and-forward

Conversation

@alpesdigital-adm

@alpesdigital-adm alpesdigital-adm commented Jun 20, 2026 •

Copy link
Copy Markdown

What

Adds REST endpoints for group/message features that baileys (the underlying lib) already implements but Evolution doesn't expose yet:

Endpoint baileys function
POST /group/memberAddMode groupMemberAddMode — restrict who adds members (admin_add / all_member_add)
POST /group/joinApprovalMode groupJoinApprovalMode — turn membership approval on/off (on / off)
GET /group/participantRequests groupRequestParticipantsList — list pending join requests
POST /group/updateParticipantRequests groupRequestParticipantsUpdate — approve/reject requests
POST /message/forwardMessage sendMessage(jid, { forward }) — forward a stored message (keeps the forwarded tag)

Why

Common group-admin needs (membership approval, locking member-add) and message forwarding. All supported by baileys (7.0.0-rc.9 in this repo) but with no HTTP surface until now.

How

Each endpoint follows the existing updateSetting / toggleEphemeral pattern: DTO + JSONSchema + router + controller + baileys service call. forwardMessage reuses the private getMessage(key, full) to load the stored WAMessage and passes it to sendMessage(jid, { forward }).

No new dependencies. tsc --noEmit + tsup build pass; pre-commit lint clean.

🤖 Generated with Claude Code

Summary by Sourcery

Expand WhatsApp group and messaging APIs while improving read-state reliability, app-state recovery, connection handling, and input protection.

New Features:

  • Expose group member-add restrictions, join approval controls, pending participant requests, and participant request updates through the REST API.
  • Add REST support for forwarding stored WhatsApp messages while preserving their forwarded state.

Bug Fixes:

  • Improve WhatsApp read-state handling for group participants, linked-device synchronization, LID/phone addressing, and group read-self events.
  • Automatically recover corrupted WhatsApp app-state collections through guarded snapshot resynchronization.
  • Preserve instance creation while preventing untrusted query parameters from overriding protected instance fields.
  • Correct persisted app-state key deserialization and reconnect after stream error 515 events.

Enhancements:

  • Add group metadata fields and throttle metadata lookups to reduce WhatsApp rate-limit pressure.
  • Upgrade Baileys and align related dependency metadata.

CI:

  • Add a release-candidate workflow that validates read-state behavior, linting, builds the application, and publishes a short-lived Docker image artifact.

Tests:

  • Add coverage for app-state self-healing, persisted key conversion, and read-state validation and synchronization.

…s + message forward

Exposes Baileys group/message capabilities that already exist in the underlying
library but had no REST endpoints:

- POST /group/memberAddMode      -> groupMemberAddMode (admin_add | all_member_add)
- POST /group/joinApprovalMode   -> groupJoinApprovalMode (on | off)
- GET  /group/participantRequests -> groupRequestParticipantsList (pending join requests)
- POST /group/updateParticipantRequests -> groupRequestParticipantsUpdate (approve | reject)
- POST /message/forwardMessage   -> sendMessage(jid, { forward }) (keeps isForwarded)

Each follows the existing pattern (DTO + JSONSchema + router + controller +
baileys service). No new dependencies — baileys 7.0.0-rc.9 already ships these.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds REST endpoints and DTO/schema/controller/service plumbing to expose new WhatsApp group admin capabilities (member-add restrictions, join approval, participant request management) and a message-forwarding endpoint that reuses stored WAMessages and Baileys forwarding, following existing validation/router patterns.

Sequence diagram for forwarding a stored WhatsApp message

sequenceDiagram
  actor Client
  participant MessageRouter
  participant SendMessageController
  participant BaileysStartupService
  participant BaileysClient

  Client->>MessageRouter: POST /message/forwardMessage
  MessageRouter->>MessageRouter: dataValidate ForwardMessageDto
  MessageRouter->>SendMessageController: forwardMessage(instance, data)
  SendMessageController->>BaileysStartupService: forwardMessage(data)
  BaileysStartupService->>BaileysStartupService: getMessage({ id: messageId }, true)
  BaileysStartupService->>BaileysClient: sendMessage(jid, { forward: fullMsg })
  BaileysClient-->>BaileysStartupService: sendMessage result
  BaileysStartupService-->>SendMessageController: forward result
  SendMessageController-->>MessageRouter: response
  MessageRouter-->>Client: 201 Created
Loading

Sequence diagram for group participant request management endpoints

sequenceDiagram
  actor Admin
  participant GroupRouter
  participant GroupController
  participant BaileysStartupService
  participant BaileysClient

  rect rgb(235, 245, 255)
    Admin->>GroupRouter: GET /group/participantRequests
    GroupRouter->>GroupRouter: groupValidate GroupJid
    GroupRouter->>GroupController: findParticipantRequests(instance, groupJid)
    GroupController->>BaileysStartupService: findParticipantRequests(groupJid)
    BaileysStartupService->>BaileysClient: groupRequestParticipantsList(groupJid)
    BaileysClient-->>BaileysStartupService: requests
    BaileysStartupService-->>GroupController: { requests }
    GroupController-->>GroupRouter: { requests }
    GroupRouter-->>Admin: 200 OK
  end

  rect rgb(235, 255, 235)
    Admin->>GroupRouter: POST /group/updateParticipantRequests
    GroupRouter->>GroupRouter: groupValidate GroupUpdateParticipantRequestDto
    GroupRouter->>GroupController: updateParticipantRequests(instance, update)
    GroupController->>BaileysStartupService: updateParticipantRequests(update)
    BaileysStartupService->>BaileysClient: groupRequestParticipantsUpdate(groupJid, participants, action)
    BaileysClient-->>BaileysStartupService: update result
    BaileysStartupService-->>GroupController: { updateParticipantRequests: result }
    GroupController-->>GroupRouter: response
    GroupRouter-->>Admin: 201 Created
  end
Loading

File-Level Changes

Change Details Files
Expose group member-add restriction and join-approval toggles via new DTOs, schemas, controller methods, and service wrappers over Baileys client APIs.
  • Introduce GroupMemberAddModeDto and GroupJoinApprovalModeDto extending GroupJid with constrained mode fields
  • Define JSON Schemas memberAddModeSchema and joinApprovalModeSchema with enum validation and non-empty constraints
  • Add GroupController methods delegating updateMemberAddMode and updateJoinApprovalMode calls to per-instance BaileysStartupService
  • Implement BaileysStartupService.updateMemberAddMode and updateJoinApprovalMode to call client.groupMemberAddMode and client.groupJoinApprovalMode and wrap responses/errors
  • Register POST /group/memberAddMode and POST /group/joinApprovalMode routes in GroupRouter using existing groupValidate flow and new schemas
src/api/dto/group.dto.ts
src/validate/group.schema.ts
src/api/controllers/group.controller.ts
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
src/api/routes/group.router.ts
Add group participant-requests listing and approval/rejection endpoints wired through schema, DTO, controller, and Baileys client wrappers.
  • Introduce GroupUpdateParticipantRequestDto with action and participants fields extending GroupJid
  • Define updateParticipantRequestSchema with enum-constrained action and non-empty, unique participants array
  • Add GroupController methods findParticipantRequests and updateParticipantRequests delegating to service layer
  • Implement BaileysStartupService.findParticipantRequests and updateParticipantRequests using client.groupRequestParticipantsList and client.groupRequestParticipantsUpdate with JID normalization, returning structured result objects
  • Register GET /group/participantRequests and POST /group/updateParticipantRequests routes in GroupRouter using groupValidate and new schemas
src/api/dto/group.dto.ts
src/validate/group.schema.ts
src/api/controllers/group.controller.ts
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
src/api/routes/group.router.ts
Introduce a message forwarding endpoint that loads a stored message and forwards it via Baileys while preserving the forwarded tag.
  • Add ForwardMessageDto carrying number and messageId
  • Define forwardMessageSchema with number and messageId validation, reusing numberDefinition and isNotEmpty
  • Register POST /message/forwardMessage in MessageRouter using dataValidate and ForwardMessageDto
  • Add SendMessageController.forwardMessage to delegate to the per-instance service
  • Implement BaileysStartupService.forwardMessage to resolve the stored WAMessage via getMessage, validate presence of message payload, normalize number to WhatsApp JID, and call client.sendMessage with a forward payload, wrapping errors as BadRequestException
src/api/dto/sendMessage.dto.ts
src/validate/message.schema.ts
src/api/controllers/sendMessage.controller.ts
src/api/routes/sendMessage.router.ts
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Possibly linked issues

  • #member_add_mode.: PR implements groupMemberAddMode via POST /group/memberAddMode, fulfilling the issue’s request for member_add_mode control.
  • #unknown: The PR adds /message/forwardMessage, implementing the requested native endpoint to forward existing WhatsApp messages.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In forwardMessage, the inner throw new BadRequestException('Message not found') is immediately caught and wrapped as a generic 'Error forwarding message', so callers lose the specific error; consider either not catching that case or rethrowing it unchanged when fullMsg?.message is missing.
  • The logic to normalize phone numbers/JIDs (e.g., number.replace(/\D/g, '') and appending @s.whatsapp.net in forwardMessage and updateParticipantRequests) appears ad hoc; consider extracting and reusing a shared helper to keep JID handling consistent across endpoints.
  • In updateParticipantRequestSchema, participants is required but not included in the isNotEmpty call, so empty strings would pass validation; it may be worth adding participants to isNotEmpty or tightening the item schema if you want to reject blank participant entries.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `forwardMessage`, the inner `throw new BadRequestException('Message not found')` is immediately caught and wrapped as a generic `'Error forwarding message'`, so callers lose the specific error; consider either not catching that case or rethrowing it unchanged when `fullMsg?.message` is missing.
- The logic to normalize phone numbers/JIDs (e.g., `number.replace(/\D/g, '')` and appending `@s.whatsapp.net` in `forwardMessage` and `updateParticipantRequests`) appears ad hoc; consider extracting and reusing a shared helper to keep JID handling consistent across endpoints.
- In `updateParticipantRequestSchema`, `participants` is required but not included in the `isNotEmpty` call, so empty strings would pass validation; it may be worth adding `participants` to `isNotEmpty` or tightening the item schema if you want to reject blank participant entries.

## Individual Comments

### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="4647" />
<code_context>
+
+  public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
+    try {
+      const participants = update.participants.map((p) => (p.includes('@') ? p : `${p}@s.whatsapp.net`));
+      const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action);
+      return { updateParticipantRequests: result };
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Participant JID normalization is inconsistent with number normalization elsewhere and may accept malformed identifiers.

Participants are treated as JIDs whenever they contain `@`, and otherwise `@s.whatsapp.net` is appended, but there’s no stripping/validation of the local part. If callers pass phone numbers with spaces, dashes, or other formatting, this will produce invalid JIDs. Please either reuse the same normalization helper used in `forwardMessage` (or similar) or enforce that inputs are already-normalized JIDs or strictly numeric IDs before appending the domain.

Suggested implementation:

```typescript
  public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
    try {
      const normalizeParticipant = (raw: string): string => {
        const value = raw.trim();

        if (!value) {
          throw new BadRequestException('Participant identifier cannot be empty');
        }

        // If it's already a JID, return as-is (after trimming)
        if (value.includes('@')) {
          return value;
        }

        // Treat as phone-like input: strip non-digits and validate
        const digitsOnly = value.replace(/\D/g, '');

        if (!digitsOnly) {
          throw new BadRequestException(
            `Invalid participant identifier "${raw}". Expected a JID or numeric phone number.`,
          );
        }

        return `${digitsOnly}@s.whatsapp.net`;
      };

      const participants = update.participants.map(normalizeParticipant);
      const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action);
      return { updateParticipantRequests: result };
    } catch (error) {
      throw new BadRequestException('Error updating participant requests', error.toString());
    }
  }

```

If your codebase already has a shared normalization helper (e.g. used in `forwardMessage`), consider:
1. Replacing the in-method `normalizeParticipant` function with a call to that shared helper for consistency.
2. If that helper returns bare numbers instead of full JIDs, adjust the logic to append `@s.whatsapp.net` only when necessary, keeping the validation/stripping behavior consistent.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
try {
const participants = update.participants.map((p) => (p.includes('@') ? p : `${p}@s.whatsapp.net`));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Participant JID normalization is inconsistent with number normalization elsewhere and may accept malformed identifiers.

Participants are treated as JIDs whenever they contain @, and otherwise @s.whatsapp.net is appended, but there’s no stripping/validation of the local part. If callers pass phone numbers with spaces, dashes, or other formatting, this will produce invalid JIDs. Please either reuse the same normalization helper used in forwardMessage (or similar) or enforce that inputs are already-normalized JIDs or strictly numeric IDs before appending the domain.

Suggested implementation:

  public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
    try {
      const normalizeParticipant = (raw: string): string => {
        const value = raw.trim();

        if (!value) {
          throw new BadRequestException('Participant identifier cannot be empty');
        }

        // If it's already a JID, return as-is (after trimming)
        if (value.includes('@')) {
          return value;
        }

        // Treat as phone-like input: strip non-digits and validate
        const digitsOnly = value.replace(/\D/g, '');

        if (!digitsOnly) {
          throw new BadRequestException(
            `Invalid participant identifier "${raw}". Expected a JID or numeric phone number.`,
          );
        }

        return `${digitsOnly}@s.whatsapp.net`;
      };

      const participants = update.participants.map(normalizeParticipant);
      const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action);
      return { updateParticipantRequests: result };
    } catch (error) {
      throw new BadRequestException('Error updating participant requests', error.toString());
    }
  }

If your codebase already has a shared normalization helper (e.g. used in forwardMessage), consider:

  1. Replacing the in-method normalizeParticipant function with a call to that shared helper for consistency.
  2. If that helper returns bare numbers instead of full JIDs, adjust the logic to append @s.whatsapp.net only when necessary, keeping the validation/stripping behavior consistent.

@NeritonDias

Copy link
Copy Markdown

🔴 👎

A feature de grupo (member-add-mode, join-approval, participant requests) parece útil e aditiva, mas o PR aponta para main. O fluxo é via develop — reabre contra develop que eu reavalio.

@dpaes

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New security issues found

Comment thread package-lock.json
Comment on lines 10565 to 10574
"node_modules/libsignal": {
"name": "@whiskeysockets/libsignal-node",
"version": "2.0.1",
"resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/libsignal/-/libsignal-6.0.0.tgz",
"integrity": "sha512-d/5V3YFtDljbFMufz4ncyUYGYhJl+vzAe+c2EFFBQ6bz1h8Q3IOMEGXYMzlibU60I+e8GagMMpji18iez3P1hA==",
"license": "GPL-3.0",
"dependencies": {
"curve25519-js": "^0.0.4",
"protobufjs": "6.8.8"
}
},
"node_modules/libsignal/node_modules/@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
"license": "MIT"
},
"node_modules/libsignal/node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
"license": "Apache-2.0"
},
"node_modules/libsignal/node_modules/protobufjs": {
"version": "6.8.8",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz",
"integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.0",
"@types/node": "^10.1.0",
"long": "^4.0.0"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts"
"protobufjs": "^7.5.5"
}
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (license/libsignal): GPL-3.0-only: Open-source license can require releasing the entire application source

This GPL-3.0-only open-source license can impose strong copyleft or non-commercial obligations that may require releasing your full application source code or restrict commercial use, depending on how the code is used or distributed

Source: trivy

@dpaes

dpaes commented Jul 13, 2026

Copy link
Copy Markdown

need to appoint to develop (not to main) @alpesdigital-adm
so we can review that

alpesdigital-adm and others added 19 commits September 9, 2026 09:21
Co-authored-by: root <root@srv1512423.hstgr.cloud>
chats.update arrives keyed by LID on accounts migrated to LID addressing,
while inbound messages are already normalized to the phone JID. Webhook
consumers could not match the read state to the conversation, so reading
on the phone never cleared unread counts downstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Webhook consumers only see the phone JID, but LID-addressed chats are
indexed by LID on the primary device. Receipts and app-state patches sent
to the phone JID target a chat the phone does not know.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The auth-state adapters rebuilt AppStateSyncKeyData with create(), which
keeps keyData as the stored base64 string. Baileys then derived the
app-state keys from the 44-char text instead of the 32 key bytes: every
incoming patch failed to decrypt and was skipped silently (MAC checks are
off by default), and every outgoing chatModify patch was encrypted with
the wrong keys, which the server answers with 401 device_removed.
fromObject decodes base64 into bytes, as Baileys useMultiFileAuthState.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(CVE pending, evolution-foundation#2435) (evolution-foundation#2549)

* security: prevent cross-instance auth bypass via query/body override (evolution-foundation#2435)

The auth guard in src/api/guards/auth.guard.ts validates instance
ownership using req.params.instanceName. The abstract router then
merged req.query (and, on /instance/create, req.body) into the
instance object via Object.assign — which silently overwrote the
already-authenticated instanceName.

An attacker with one valid token could send:

  GET /chat/findMessages/MY_INSTANCE?instanceName=VICTIM_INSTANCE

Auth passed for MY_INSTANCE, but dataValidate() then replaced the
instance with VICTIM_INSTANCE before execute() ran — giving the
caller full access to read/send messages, modify settings, and
delete other tenants' instances.

CWE-639: Authorization Bypass Through User-Controlled Key

Fix: introduce sanitizeUntrustedInput() that strips
PROTECTED_INSTANCE_FIELDS (instanceName, instanceId) from any
untrusted source before merging into the instance object. Logs a
warning on attempts so abuse is auditable.

Closes evolution-foundation#2435
Reported by @lighthousekeeper1212 via static analysis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(lint): remove extra blank line introduced by recent merge

Pre-push lint flagged whatsapp.baileys.service.ts:531 (double blank
line after the stream:error 515 fix merged via evolution-foundation#2509). Trivial
prettier fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a55a2b)
…m:error 515 (evolution-foundation#2509)

* fix(baileys): prevent healthy instances from being killed after stream:error 515

When WhatsApp sends stream:error code=515 (Connection Replaced), Baileys
handles the reconnect correctly and fires connection.update with state='open'.
However, WhatsApp then sends a 401 (loggedOut) to clean up the old session
slot, which Evolution API incorrectly treated as a real logout, killing the
newly-connected healthy instance.

The fix tracks when a stream:error 515 node arrives via the CB:stream:error
WebSocket event. If a loggedOut (401) close event fires within 30 seconds of
a 515, it is treated as a transient reconnect rather than a real logout.

Fixes evolution-foundation#2498

* fix(baileys): name the 515 reconnect grace + tighten stream-error types

Addresses sourcery-ai review feedback on the previous commit:

- Extract the 30 000ms reconnect grace window into a named class constant
  STREAM_515_RECONNECT_GRACE_MS so future tuning is self-documenting
  rather than a literal scattered through the close handler.
- Extract the magic '515' string into STREAM_ERROR_CODE_RECONNECT.
- Replace the loose 'node: any' on the 'CB:stream:error' handler with a
  minimal structural type ({ attrs?: { code?: string | number } }) so
  the payload shape is documented and type-checked.
- Compare the code via String(...) so a numeric 515 from the underlying
  socket library still triggers the grace window — the original literal
  '515' check would have silently broken on a type change.

* fix(baileys): satisfy prettier — collapse 515 reconnect guard onto one line

Check Code Quality lint failed on prettier/prettier (the
recentStream515 expression fits within the project's 120-col
printWidth on a single line, while shouldReconnect still needs to
break across two).

Co-authored-by: Octopus <liyuan851277048@icloud.com>

---------

Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: octo-patch <octo-patch@users.noreply.github.com>
(cherry picked from commit 1d38d4f)
sanitizeUntrustedInput on /instance/create removed instanceName from the
body, which is the only source of the new instance name. Create has no
authenticated instance in the path, so there is nothing to override there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nests the tc token under the picture query in profilePictureUrl, so
profile pictures of privacy-restricted contacts resolve. The Android
browser mode is opt-in by browser name and stays off here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
History sync after a fresh pairing (and some live messages) deliver
LID-only keys. Without the phone JID, webhook consumers cannot identify
the sender and drop the message. Resolve it with getPNForLID, live and in
messaging-history.set; unknown mappings leave the key untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
History messages without a known contact got pushName = JID user part
(LID digits included), which CRMs then stored as the contact name. Look
the contact up by the phone JID as well and otherwise leave it empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Em grupo o recibo de leitura endereca quem enviou, nao a conversa: o Baileys
agrupa recibo por `remoteJid:participant` e recusa um markRead cujo key de
mensagem recebida nao tenha participant ("Expected not from me message to have
participant", Utils/chat-utils). A key chegava aqui e era remontada sem esse
campo, entao nenhuma marcacao de leitura de grupo podia funcionar.

Agora o campo e declarado no DTO e no schema de entrada, repassado como veio do
provider, e a ausencia dele vira 400 com o campo nomeado em vez de erro opaco
do SDK — mas so no caminho com lastMessage, que e o que o Baileys realmente
recusa. Pedido legado so com recibo (Chatwoot) segue aceito como antes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxnyvfDrgNppE1PftV4gTa
…-self, keep participant on read

1. App-state self-heal. Baileys decodePatches only warns on an LTHash mismatch
   ("LTHash verification failed, skipping remaining patches"), then returns the
   already-advanced version with the mismatching hash and the caller persists it.
   Every later patch then fails the same way, so phone read/unread changes
   (regular_low) never become chats.update. The logger handed to makeWASocket is
   now wrapped (Proxy, works at any log level, covers child loggers) to spot that
   exact warning; a per-instance poller (outside Baileys' sync transaction) then
   clears app-state-sync-version-<name> through the socket key store (null, in a
   transaction on the same mutex key as resyncAppState, so the in-memory cache and
   Redis are both cleared) and runs resyncAppState([name], false) so the snapshot
   rebuilds the hash and its mutations still flow to chats.update. Guards: socket
   must be open, 10 min cooldown and 3 attempts/hour per collection, WARN log
   "app-state self-heal: <instance> <collection> v<old> -> snapshot" plus outcome.
   The snapshot variant of the warning is deliberately ignored. MAC verification
   stays enabled.

2. Group read-self. Baileys emits message-receipt.update (not messages.update) for
   a group read; the reader is receipt.userJid (the node's participant), key.fromMe
   is always true. When the reader is the instance's own account (phone JID or LID,
   device suffix stripped) and groupsIgnore is off, send one MESSAGES_UPDATE READ
   webhook per group per batch (fromMe=false, keyId = newest message of the batch)
   so the CRM group fallback fires. Receipts from other participants are ignored.
   updateMessagesReadedByTimestamp is unchanged.

3. syncMessageReadState keeps `participant` on readMessages keys (needed by
   WhatsApp for group receipts); DTO and request schema accept it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or leave the collection cleared

1. A resync from a cleared version has no minimum version, so Baileys rc14
   (processSyncAction, isInitialSync=false, no `conditional`) re-emits every
   record of the snapshot: stale markChatAsRead -> chats.update unreadCount 0/-1
   that the CRM applies as "observed now" (clears chats that still have unread
   messages / re-marks read ones unread); regular_high replays chats.delete and
   messages.delete for every chat/message ever deleted. Events chats.update,
   chats.delete, chats.lock, messages.update and messages.delete emitted from
   inside the heal resync are now dropped (AsyncLocalStorage scope + guard on
   socket.ev.emit, so concurrent live events pass); the count is logged.
   isInitialSync=true was not used: its conditional chat updates stay buffered
   and swallow later live updates for the same chats.

2. If the heal resync stores nothing (missing key -> parked, giving up) or
   throws, the previous state is written back (only if nothing was stored
   meanwhile, same tx key). Leaving it cleared made the next ordinary resync
   (server_sync / key arrival, isInitialSync=false) replay the whole snapshot
   unguarded.

Tests: 39 node:test pass; tsc clean against baileys 7.0.0-rc13 (local) and
7.0.0-rc14 (production pin) types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WhatsApp now delivers some accounts' contact statuses as a <status> stanza
instead of <message from="status@broadcast">. Baileys 7.0.0-rc14/master
registers no CB:status handler, so it is never acked; ~50 min after the
connection opens the server closes the stream with
<stream:error><ack class="status" type="media" id=.../></stream:error>
(Stream Errored (ack) -> badSession 500), redelivers the same pending stanza
on reconnect, and the drop repeats. readStatus/shouldIgnoreJid do not matter:
the stanza never reaches handleMessage.

Register ws.on('CB:status') after makeWASocket and send a plain ack (class =
node tag, id, to, participant/recipient/type, no error attribute, same as
Baileys' buildAckStanza; a NACK would keep it pending). Same fix as upstream
WhiskeySockets/Baileys#2858 / PR #2866 and whatsmeow a238105488; remove once a
Baileys release includes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loop

- attachStatusAck skips (returns false) when the socket already has a CB:status
  listener, i.e. a Baileys release that ships #2866 and acks the stanza itself;
  the service logs that the module can be dropped.
- Baileys' sendNode encodes synchronously before returning the send promise, so
  a sync throw is now caught and reported instead of escaping ws.emit.

Verified against a real 7.0.0-rc14 socket: no CB:status listener of its own,
the ack goes out as <ack id to class='status' type='media'/>, identical to the
PR #2866 test and whatsmeow sendAck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Baileys onWhatsApp only uses the USync contact protocol, so the endpoint never
returned the account LID. With `includeLid: true`, confirmed numbers (exists)
are resolved in ONE lidMapping.getLIDsForPNs call (cache -> auth keys -> USync
LID query) and each item gets `lid: "<digits>@lid"` or null.

- default response unchanged when the flag is absent
- lookup is keyed by the jid WhatsApp confirmed (9th-digit variants)
- capped at 50 numbers per request; the rest return null with a WARN
- a failing lookup logs WARN and returns lid null, never fails the request
- runs after the IsOnWhatsapp cache write; that column keeps its 'lid' marker

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants